﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MonoStrategy
{
    /// <summary>
    /// Jobs are used by movables to do tasks like wood cutting, carrying, building, fighting,
    /// stealing, etc. They provide a generic way to accomplish these tasks in a synchronized,
    /// stable and straighforward manner. Each movable can only have one job at maximum. If it
    /// does, it is considered non-free and thus not eligable to get another job. The job itself
    /// is permanent and thus gets repeated if <see cref="Prepare"/> return "true".
    /// </summary>
    public abstract class GenericJob : IDisposable
    {
        protected static readonly CrossRandom m_Random = new CrossRandom(0);

        private readonly LinkedList<AnimationStep> m_Steps = new LinkedList<AnimationStep>();
        private LinkedListNode<AnimationStep> m_CurrentStep = null;
        private String m_AnimationClass;

        private readonly UniqueIDObject m_Unique;
        internal long UniqueID { get { return m_Unique.UniqueID; } }
        public String AnimationClass
        {
            get { return m_AnimationClass; }
            internal set
            {
                if (m_AnimationClass == value)
                    return;

                m_AnimationClass = value;

                RaiseAnimClassChanged();
            }
        }
        public Movable Movable { get; private set; }
        internal MovableManager MovMgr { get { return Movable.Parent; } }

        public event Procedure<GenericJob> OnAnimationClassChanged;

        private class AnimationStep : IDisposable
        {
            internal Point Target { get; private set; }
            internal int Millis { get; set; }
            internal bool IsCompleted { get; private set; }
            internal long StartCycles { get;  set; }
            internal Func<bool, bool> OnCompleted { get; private set; }
            internal Func<bool> OnStarted { get; private set; }
            internal Procedure<Procedure> CustomAnimPlayback { get; private set; }

            internal void MarkAsCompleted()
            {
                IsCompleted = true;
                OnCompleted = null;
            }

            public void Dispose()
            {
                OnCompleted = null;
            }

            internal AnimationStep(Procedure<Procedure> inCustomAnimPlayback)
            {
                if (inCustomAnimPlayback == null)
                    throw new ArgumentNullException();

                CustomAnimPlayback = inCustomAnimPlayback;
            }

            internal AnimationStep(
                Point inTarget,
                Func<bool> onStepStarted,
                Func<bool, bool> onStepCompleted)
            {
                Target = inTarget;
                OnCompleted = onStepCompleted;
                OnStarted = onStepStarted;
            }

            internal AnimationStep(
                int inMillis, 
                Func<bool> onStepStarted,
                Func<bool, bool> onStepCompleted)
            {
                if (inMillis <= 0)
                    throw new ArgumentOutOfRangeException();

                Millis = inMillis;
                OnCompleted = onStepCompleted;
                OnStarted = onStepStarted;
            }
        }

        protected void RaiseAnimClassChanged()
        {
            if (OnAnimationClassChanged != null)
                OnAnimationClassChanged(this);
        }

        internal GenericJob(Movable inMovable)
        {
            if (inMovable == null)
                throw new ArgumentNullException();

            m_Unique = new UniqueIDObject(this);
            Movable = inMovable;
        }

        /// <summary>
        /// should be overwritten in a subclass. The task here is to determine whether the job
        /// can be executed by now and if so, returns "true", "false" otherwise. Further, one
        /// should add some initial animation steps, otherwise the job will finish immediately.
        /// </summary>
        /// <returns></returns>
        internal abstract bool Prepare();

        /// <summary>
        /// Asynchronously aborts the job on next update. 
        /// </summary>
        internal void Stop()
        {
            if (m_CurrentStep != null)
            {
                var onCompleted = m_CurrentStep.Value.OnCompleted;

                m_CurrentStep.Value.Dispose();

                if (onCompleted != null)
                    onCompleted(false);
            }

            m_CurrentStep = null;
            m_Steps.Clear();
        }

        public void Dispose()
        {
            Stop();
        }

        /// <summary>
        /// Lets the movable walk to the given destination and call <paramref name="onStepCompleted"/>
        /// with "true" if the target has been reached and "false" otherwise. The callback should
        /// return "true" if the job should continue and "false" if the job should be aborted. You
        /// may add new animation steps within the callback! Animation steps are removed from the
        /// internal list, once they were executed (regardless of success/failure).
        /// </summary>
        internal void AddAnimationStep(
            Point inTarget,
            Func<bool> onStepStarted,
            Func<bool, bool> onStepCompleted)
        {
            m_Steps.AddLast(new AnimationStep(inTarget, onStepStarted, onStepCompleted));
        }

        /// <summary>
        /// Allows the caller to inject a fully customized animation step into the timeline.
        /// The step is completed, when the given lambda expression in return raises the 
        /// passed animation completion handler.
        /// </summary>
        internal void AddAnimationStep(Procedure<Procedure> inCustomAnimPlayback)
        {
            m_Steps.AddLast(new AnimationStep(inCustomAnimPlayback));
        }

        /// <summary>
        /// Calls <paramref name="onStepStarted"/> when animation has reached this step and if
        /// "true" is returned by the callback it will just wait the given time in millis rounded
        /// up to the next cycle time boundary. Afterwards <param name="onStepCompleted"/> is called.
        /// The callback should return "true" if the job should continue and "false" if the job 
        /// should be aborted. You may add new animation steps within the callback! Animation steps 
        /// are removed from the internal list, once they were executed (regardless of success/failure).
        /// </summary>
        internal void AddAnimationStepWithPathFollow(int inMillis, Func<bool> onStepStarted, Func<bool, bool> onStepCompleted)
        {
            inMillis = (int)(((inMillis + MovMgr.CycleResolution - 1) / MovMgr.CycleResolution) * MovMgr.CycleResolution);

            if (inMillis <= 0)
                throw new ArgumentOutOfRangeException();

            m_Steps.AddLast(new AnimationStep(inMillis, onStepStarted, onStepCompleted));
        }

        /// <summary>
        /// Is used internally to update the job status.
        /// </summary>
        internal void Update()
        {
            bool wasRunning = false;

            if (m_CurrentStep != null)
            {
                wasRunning = true;

                // check if animation step is completed
                var step = m_CurrentStep.Value;

                if (step.Millis > 0)
                {
                    if (step.Millis < (MovMgr.CurrentCycle - step.StartCycles) * CyclePoint.CYCLE_MILLIS)
                    {
                        var onCompleted = step.OnCompleted;

                        step.Dispose();
                        m_CurrentStep = null;

                        if (onCompleted != null)
                        {
                            if (!onCompleted(true))
                            {
                                Stop();
                                return;
                            }
                        }
                    }
                }
                else if (step.IsCompleted)
                {
                    m_CurrentStep = null;
                }
            }

            // is used for both, starting a new job and selecting the next step
            if (m_CurrentStep == null)
            {
                // check if job can be restarted
                if (m_Steps.Count == 0)
                    wasRunning = false;

                if (!wasRunning && !Prepare())
                    return;

                if (m_Steps.Count > 0)
                {
                    m_CurrentStep = m_Steps.First;
                    m_Steps.RemoveFirst();

                    var step = m_CurrentStep.Value;

                    if ((step.OnStarted != null) && !step.OnStarted())
                    {
                        m_CurrentStep = null; // prevent onCompleted notification
                        Stop();

                        return;
                    }

                    if (step.Millis > 0)
                    {
                        step.StartCycles = MovMgr.CurrentCycle;

                        MovMgr.QueueWorkItem(step.Millis, () =>
                        {
                            Update();
                        });
                    }
                    else if (step.CustomAnimPlayback == null)
                    {
                        /*
                         * For sake of continous animation, we should now be at a time cycle where path
                         * requests will immediately be dispatched. Otherwise the movable will walk or
                         * stand without changing its position for some noticeable time.
                         */
                        // TODO:
                        //if (!MovMgr.IsAlignedCycle)
                        //    throw new InvalidOperationException("For flawless animation, one must ensure that path planning animation steps are aligned to the movable manager's cycle resolution.");

                        MovMgr.SetPath(Movable, step.Target, (succeeded) =>
                        {
                            // handle completion
                            if (!succeeded)
                            {
                                Stop();
                                return;
                            }

                            var onCompleted = step.OnCompleted;

                            step.Dispose();

                            if (onCompleted != null)
                            {
                                if (!onCompleted(true))
                                {
                                    m_Steps.Clear();
                                    return;
                                }
                            }

                            step.MarkAsCompleted();

                            Update();
                        });
                    }
                    else
                    {
                        step.CustomAnimPlayback(() =>
                        {
                            step.MarkAsCompleted();
                        });
                    }
                }
            }
        }
    }
}
